﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using RuleOfReady.Core.DomainModels.Tiles;
using RuleOfReady.Core.DomainModels.Sets;
using RuleOfReady.Core.Engines;
using RuleOfReady.Core.Utility;

namespace RuleOfReady.HandAnalyser
{
    class HandAnalyser
    {
        const int handSize = 13;
        const string indent = "   ";

        static TileEngine engine;

        static void Main(string[] args)
        {
            ConsoleKeyInfo keyInfo;
            engine = new TileEngine();
            do
            {
                Console.Clear();
                DrawAndAnalyseHand();
                Console.WriteLine("Press space for another, or any other key to exit");
                keyInfo = Console.ReadKey();
            } while (keyInfo.Key == ConsoleKey.Spacebar);
        }

        private static void DrawAndAnalyseHand()
        {
            Console.WriteLine("Drawing a random hand:");
            Console.WriteLine();

            IList<MahjongTile> tileSet = engine.CreateGameTileSet();

            tileSet.Shuffle();

            LinkedList<MahjongTile> wall = new LinkedList<MahjongTile>(tileSet);
            IEnumerable<MahjongTile> savedHand = DrawHand(wall);
            IList<MahjongTile> hand = new List<MahjongTile>(savedHand);

            StringBuilder sb = new StringBuilder();
            Console.WriteLine("Drawn Tiles (sorted):");
            sb.Append(indent);
            for (int ctr = 1; ctr <= hand.Count; ctr++)
                sb.Append(string.Format("{0,2} ", ctr));
            Console.WriteLine(sb);
            sb.Clear();
            sb.Append(indent);
            foreach (MahjongTile tile in hand.OrderBy(tile => tile))
                sb.Append(tile).Append(" ");
            Console.WriteLine(sb);
            Console.WriteLine();

            Console.WriteLine();


            //check for 13 orphans
            Dictionary<MahjongTile, int> majorTileCounts = new Dictionary<MahjongTile, int>();
            foreach (MahjongTile tile in savedHand.Where(tile => engine.MajorTiles.Contains(tile)))
            {
                if (majorTileCounts.ContainsKey(tile))
                    majorTileCounts[tile] += 1;
                else
                    majorTileCounts[tile] = 1;
            }
            int thirteenWondersShanten = 14 - majorTileCounts.Count;
            if (majorTileCounts.Values.Where(count => count > 1).Any())
                thirteenWondersShanten += 1;

            //first pull out any pairs
            IList<MahjongPair> uniquePairs = PullOutUniquePairs(hand);

            // special hand, seven unique pairs (Japanese: 'chiitoitsu')
            int sevenPairsShanten = 6 - uniquePairs.Count;

            IList<MahjongPartialSequence> partialSequencesInHand = PullOutPartialSequences(hand);

            PartialSequenceComparer comparer = new PartialSequenceComparer();
            HashSet<MahjongPartialSequence> uniquePartialSequences =
                new HashSet<MahjongPartialSequence>(partialSequencesInHand);

            IList<MahjongPossibleSet> twoTileSets =
                uniquePartialSequences.Cast<MahjongPossibleSet>().Concat(uniquePairs.Cast<MahjongPossibleSet>()).ToList();

            IList<MahjongPossibleSet> completedSets;

            completedSets = new List<MahjongPossibleSet>();

            // if there are more than pairs or partial sequences, then try to combine two of them
            if (uniquePartialSequences.Count + uniquePairs.Count > 5)
            {
                completedSets = new List<MahjongPossibleSet>();
                Dictionary<MahjongPossibleSet, IEnumerable<MahjongPossibleSet>> setWaitLookup =
                    new Dictionary<MahjongPossibleSet, IEnumerable<MahjongPossibleSet>>();
                foreach (MahjongPossibleSet set in twoTileSets)
                {
                    IEnumerable<MahjongPossibleSet> setsHavingWaitingTiles =
                        twoTileSets.Except(new[] { set })
                                   .Where(otherSet => otherSet.Tiles.Where(otherTile => set.WaitingTiles.Contains(otherTile))
                                                                    .Any());
                    setWaitLookup[set] = setsHavingWaitingTiles;
                }

                var mergeSet = setWaitLookup.Where(kvp => kvp.Value.Any()).Select(kvp => kvp.Key).FirstOrDefault();
                if (mergeSet != null)
                {
                    var otherSet = setWaitLookup[mergeSet].First();
                    MahjongTile tileToMerge = otherSet.Where(tile => mergeSet.WaitingTiles.Contains(tile)).First();
                    IList<MahjongTile> otherSetList = otherSet.ToList();
                    otherSetList.Remove(tileToMerge);
                    MahjongTile otherTile = otherSetList.First();
                    //note: otherTile

                    twoTileSets.Remove(mergeSet);
                    twoTileSets.Remove(otherSet);
                    completedSets.Add(mergeSet.Add(tileToMerge));
                    hand.Add(otherTile);
                }
            }

            // try to add leftover tiles into the twotile sets
            foreach (MahjongPossibleSet set in twoTileSets.ToList())
            {
                MahjongTile tileToAdd = hand.Where(tile => set.WaitingTiles.Contains(tile)).FirstOrDefault();
                if (tileToAdd != null)
                {
                    twoTileSets.Remove(set);
                    hand.Remove(tileToAdd);
                    completedSets.Add(set.Add(tileToAdd));
                }
            }

            int standardShanten = hand.Count;
            // fill out missing sets (of the 4 sets and a pair) with single tiles
            int standardSetCount = twoTileSets.Count + completedSets.Count;

            // if there are 5 (or more) sets, one has to be a pair
            if (standardSetCount >= 5
                && !twoTileSets.OfType<MahjongPair>().Any())
                //if you have 5 possible sets with no pair, but has a triple, make that triple into a pair
                //   (this is a rather extreme edge case)
                if (completedSets.OfType<MahjongTriple>().Any())
                {
                    MahjongPossibleSet tripleToTakeApart = completedSets.OfType<MahjongTriple>().First();
                    completedSets.Remove(tripleToTakeApart);
                    MahjongTile pairTile = tripleToTakeApart.Tiles.First();
                    MahjongPair newPair = new MahjongPair(pairTile);
                    twoTileSets.Add(newPair as MahjongPossibleSet);
                    hand.Add(pairTile);
                }
                else
                    standardSetCount -= 1;
            int singleTileSets = 0;
            if (standardSetCount < 5)
                singleTileSets = 5 - standardSetCount;
            standardShanten -= singleTileSets;
            int shanten;
            if (thirteenWondersShanten < sevenPairsShanten && thirteenWondersShanten < standardShanten)
            {
                shanten = thirteenWondersShanten;
                IEnumerable<MahjongTile> tiles = majorTileCounts.Keys;
                Console.WriteLine("13 orphans:");
                Console.WriteLine(indent + string.Join(" ", tiles));
            }
            else
                if (sevenPairsShanten < standardShanten)
                {
                    shanten = sevenPairsShanten;
                    DisplayFoundSets(uniquePairs, "7 pairs hand:", suppressWaits: true);
                }
                else
                {
                    shanten = standardShanten;
                    DisplayFoundSets(completedSets, string.Format("Complete Sets: ({0} tiles)", completedSets.Count * 3), suppressWaits: true);
                    DisplayFoundSets(twoTileSets, string.Format("Pair and/or partial sets: ({0} tiles)", twoTileSets.Count * 2));
                    if (singleTileSets > 0)
                        Console.WriteLine("{0} set{1} with only a single tile", singleTileSets, singleTileSets == 1 ? "" : "s");
                    Console.WriteLine();
                    Console.WriteLine("Remaining tiles: ({0} tiles)", hand.Count);
                    Console.WriteLine(string.Join(" ", hand.OrderBy(tile => tile)));
                }

            Console.WriteLine();
            Console.WriteLine("Shanten Number: {0}", shanten);
            Console.WriteLine();
        }

        private static void DisplayFoundSets(IEnumerable<MahjongPossibleSet> possibleSets, string title = "", 
                                             bool suppressWaits = false)
        {
            if (!string.IsNullOrEmpty(title))
                Console.WriteLine(title);

            foreach (MahjongPossibleSet set in possibleSets)
            {
                
                Console.Write(indent + set);
                if (set.WaitingTiles.Any() && !suppressWaits)
                    Console.WriteLine("  waiting on -->  " + string.Join(" ", set.WaitingTiles));
                else
                    Console.WriteLine();
            }

            Console.WriteLine();
        }

        static IEnumerable<MahjongTile> DrawHand(LinkedList<MahjongTile> wall)
        {
            for (int ctr = 0; ctr < handSize; ctr++)
            {
                yield return wall.PopFirst();
            }
        }

        static IList<MahjongSequence> PullOutSequences(ICollection<MahjongTile> hand)
        {
            List<MahjongSequence> sequencesFound = new List<MahjongSequence>();

            foreach (MahjongSequence sequence in engine.Sequences)
            {
                HashSet<MahjongTile> sequenceSet = new HashSet<MahjongTile>(sequence);
                if (sequenceSet.IsSubsetOf(hand))
                {
                    sequencesFound.Add(sequence);
                    foreach (MahjongTile tile in sequence)
                        hand.Remove(tile);
                }
            }
            return sequencesFound;
        }

        static IList<MahjongTriple> PullOutTriples(ICollection<MahjongTile> hand)
        {
            List<MahjongTriple> triplesFound = new List<MahjongTriple>();
            foreach (MahjongTile tile in hand.ToList())
            {
                if (hand.Where(t => t == tile).Count() >= 3)
                {
                    triplesFound.Add(new MahjongTriple(tile));
                    for (int ctr = 0; ctr < 3; ctr++)
                    {
                        hand.Remove(tile);
                    }
                }
            }
            return triplesFound;
        }

        static IList<MahjongPair> PullOutUniquePairs(ICollection<MahjongTile> hand)
        {
            List<MahjongPair> pairsFound = new List<MahjongPair>();
            foreach (MahjongTile tile in hand.ToList())
            {
                if (hand.Where(t => t == tile).Count() >= 2)
                {
                    MahjongPair pair = new MahjongPair(tile);
                    if (!pairsFound.Contains(pair))
                    {
                        pairsFound.Add(pair);
                        for (int ctr = 0; ctr < 2; ctr++)
                        {
                            hand.Remove(tile);
                        }
                    }
                }
            }
            return pairsFound;
        }

        static IList<MahjongPartialSequence> PullOutPartialSequences(ICollection<MahjongTile> hand)
        {
            List<MahjongPartialSequence> partialSequencesFound = new List<MahjongPartialSequence>();

            foreach (MahjongPartialSequence partialSequence in engine.PartialSequences)
            {
                HashSet<MahjongTile> partialSet = new HashSet<MahjongTile>(partialSequence);
                if (partialSet.IsSubsetOf(hand))
                {
                    partialSequencesFound.Add(partialSequence);
                    foreach (MahjongTile tile in partialSequence)
                        hand.Remove(tile);
                }
            }
            return partialSequencesFound;
        }

    }
}
